# vBulletin Poll interaction
# by Cee Pee

import forumapi
from deps.Form import Form

__all__ = [
	# Exceptions
	"PollError",
	
	# Extensible objects
	"Poll", "PollOption",
]

## Exceptions ##

class PollError(forumapi.ForumAPIException):
	"""Indicates a failed attempt at retrieving poll data, or voting."""

## Hooks ##

def _retrieve_metadata(session=None, soup=None, loc=None, t=None, **kwargs):
	if loc == "thread":
		poll = Poll(session, thread=t)
		try:
			poll.retrieve_thread(soup=soup)
			t._poll = poll
		except PollError:
			t._poll = False

def _get_threads_list(session=None, thread=None, posticon_url=None, **kwargs):
	if "poll" in posticon_url:
		thread._poll = Poll(session, thread=thread)
	else:
		thread._poll = False

forumapi.hooks.add("vb_retrieve_metadata", _retrieve_metadata)
forumapi.hooks.add("vb_get_threads_list", _get_threads_list)

## Classes ##

class Poll(forumapi.ExtensibleObject):
	"""A thread poll. It has a poll ID and thread associated with it, and is
	able to retrieve its question, options, and other metadata. It can also be
	voted on."""
	
	@classmethod
	def obj(cls, session, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], int):
			return(cls(session, *a[1:], id=a[0], **kw))
		elif a[0] == False:
			return(False)
		else:
			return(None)
	
	def __init__(self, session, id=None, thread=None, question=None, options=None, closing_time=None, multiple=None, public=None, total_votes=None, vote_allowed=None):
		if not (id or thread):
			raise(ValueError("ID or thread required."))
		
		self.session = session
		
		self._id = id
		
		self._thread = self.module.get_attr("Thread").obj(session, thread)
		
		self._question = question
		
		if isinstance(options, list):
			self._options = [PollOption.obj(self, option) for option in options]
		else:
			self._options = None
		
		self._closing_time = closing_time
		
		self._multiple = multiple
		self._public = public
		
		self._total_votes = total_votes
		
		self._form = None
		
		self._vote_allowed = vote_allowed
		
		self._find_instances()
	
	def _merge(self, other):
		if self._id:
			other._id = self._id
		
		self._merge_obj(other, "_thread")
		self._merge_obj(other, "_question")
		self._merge_list(other, "_options")
		
		if self._closing_time is not None:
			other._closing_time = self._closing_time
		
		if self._multiple is not None:
			other._multiple = self._multiple
		if self._public is not None:
			other._public = self._public
		
		if self._total_votes is not None:
			other._total_votes = self._total_votes
		
		if self._vote_allowed is not None:
			other._vote_allowed = self._vote_allowed
	
	@property
	def data(self):
		return({
			"id": self._id,
			"thread": int(self.thread) if self._thread else None,
			"question": foumapi._flatten_data(self._question),
			"options": [int(option) for option in self.options] if self._options is not None else None,
			"closing_time": self._closing_time,
			"multiple": self._multiple,
			"public": self._public,
			"total_votes": self._total_votes,
			"vote_allowed": self._vote_allowed,
		})
	
	@property
	def id(self):
		if self._id:
			return(self._id)
		else:
			self.retrieve_thread()
			return(self._id)
	
	@property
	def thread(self):
		if self._thread:
			return(self._thread)
		else:
			self.retrieve_results()
			return(self._thread)
	
	@property
	def question(self):
		if self._question is not None:
			return(self._question)
		elif self._id:
			self.retrieve_results()
			return(self._question)
		elif self._thread:
			self.retrieve_thread()
			return(self._question)
	
	@property
	def options(self):
		if self._options is not None:
			return(self._options)
		elif self._id:
			self.retrieve_results()
			return(self._options)
		elif self._thread:
			self.retrieve_thread()
			return(self._options)
	
	@property
	def closing_time(self):
		if self._closing_time:
			return(self._closing_time)
		elif self._id:
			self.retrieve_results()
			return(self._closing_time)
		elif self._thread:
			self.retrieve_thread()
			return(self._closing_time)
	
	@property
	def multiple(self):
		if self._multiple is not None:
			return(self._multiple)
		elif self._id:
			self.retrieve_results()
			return(self._multiple)
		elif self._thread:
			self.retrieve_thread()
			return(self._multiple)
	
	@property
	def public(self):
		if self._public is not None:
			return(self._public)
		else:
			# We might be able to check results for blocks of users, but meh
			self.retrieve_thread()
			return(self._public)
	
	@property
	def total_votes(self):
		if self._total_votes is not None:
			return(self._total_votes)
		else:
			self.retrieve_results()
			return(self._total_votes)
	
	@property
	def form(self):
		if self._form:
			return(self._form)
		else:
			self.retrieve_thread()
			return(self._form)
	
	@property
	def vote_allowed(self):
		if self._vote_allowed is not None:
			return(self._vote_allowed)
		else:
			self.retrieve_thread()
			return(self._vote_allowed)
	
	def retrieve_thread(self, soup=None):
		"""Retrieve the poll from a thread page. Can also be given a
		`BeautifulSoup` of a thread page."""
		
		if not soup:
			page = self.session.get_page("showthread.php?t={0}".format(self.thread.id))
			soup = self.session.soup(page)
		
		form_soup = soup.find("form", action=lambda url: "poll.php" in url)
		if not form_soup:
			# No poll, or not allowed to vote. Try finding a result on the page.
			self._vote_allowed = False
			tborder = soup.findAll("table", "tborder")
			for table in tborder:
				if table.find("td", title="Votes"):
					self.retrieve_results(soup=table)
					return(None)
			
			raise(PollError("No poll found."))
		
		self._vote_allowed = True
		
		if not self._form:
			self._form = Form(form_soup, session=self.session)
		
		if not self._id:
			self._id = int(self.form["pollid"])
		
		if not self._closing_time:
			self._closing_time = self.get_time(form_soup)
		
		panel = form_soup.find("div", "panel")
		
		if self._public is None:
			# "Be advised that this is a public poll..."
			if panel.find("div", "fieldset"):
				self._public = True
			else:
				self._public = False
		
		if self._question is None:
			self._question = self.module.get_attr("BBCode")(self.session, panel.find("strong"))
		
		if self._options is None:
			self._options = []
			for control in self.form.controls:
				if control.id != False and "cb_optionnumber_" in control.id:
					if control.type == "checkbox":
						self._multiple = True
					else:
						self._multiple = False
					
					if isinstance(control.label, str):
						name = control.label
					else:
						name = self.module.get_attr("BBCode")(self.session, control.label)
					
					number = int(control.id[16:])
					
					option = PollOption(self.session, self, name, number)
					# Instances tracking takes care of merging if it exists
					if option not in self._options:
						self._options.append(option)
	
	def retrieve_results(self, soup=None):
		"""Retrieve the results page of the poll. Can also be given a
		`BeautifulSoup` of a results table."""
		
		from_results = False
		if soup is None:
			from_results = True
			page = self.session.get_page("poll.php?do=showresults&pollid={0}".format(self.id))
			page_soup = self.session.soup(page)
			
			if not self._thread:
				breadcrumb = self.session.get_breadcrumb(soup)
				if isinstance(breadcrumb[-1], forumapi.Thread):
					self._thread = breadcrumb[-1]
			
			for table in page_soup.findAll("table", "tborder"):
				head = table.find("td", "tcat")
				if head is not None:
					soup = table
					break
			
			if soup is None:
				raise(PollError("No poll found."))
		else:
			head = table.find("td", "tcat")
		
		if self._question is None:
			question = head.find("span", "normal")
			question.contents[0] = question.contents[0][2:]
			self._question = self.module.get_attr("BBCode")(self.session, question)
		
		if self._closing_time is None:
			self._closing_time = self.get_time(soup)
		
		if self._options is None:
			self._options = []
		
		option_rows = soup.findAll("tr")[1:-1]
		
		number = 0
		for option_row in option_rows:
			number += 1
			
			cells = option_row.findAll("td")
			
			voted = cells[0].find("em")
			if voted:
				# <em>Option we voted on earlier</em>
				name = self.module.get_attr("BBCode")(self.session, voted)
				self._vote_allowed = False
			else:
				name = self.module.get_attr("BBCode")(self.session, cells[0])
			
			voters = None
			if from_results:
				voters = []
				users = cells[0].find("div")
				if users:
					self._public = True
					for voter_link in users:
						if isinstance(voter_link, str):
							# Comma delimiter, skip
							continue
						
						user_id = forumapi.qs_dict_from_url(voter_link["href"])["u"][0]
						user_name = None
						
						if voter_link.string is not None:
							user_name = str(voter_link.string)
						elif voter_link.contents[0].string is not None:
							user_name = str(voter_link.contents[0].string)
						
						voters.append(forumapi.User(self.session, id=user_id, name=user_name))
			
			if cells[2].contents[0].string is not None:
				# <td><strong>#</strong></td>
				votes = int(cells[2].contents[0].string)
			elif cells[2].contents[0].contents[0].string is not None:
				# Results on thread: <td><strong><a>#</a></strong></td>
				votes = int(cells[2].contents[0].contents[0].string)
				poll_link = cells[2].contents[0].contents[0]
				self._id = forumapi.qs_dict_from_url(poll_link["href"])["pollid"][0]
			
			percentage = str(cells[3].string)
			
			option = PollOption(self.session, self, number, name=name, votes=votes, percentage=percentage, voters=voters)
			# Instances tracking takes care of merging if it exists
			if option not in self._options:
				self._options.append(option)
		
		if self._total_votes is None:
			self._total_votes = int(soup.find("td", "tfoot").find("strong").string)
	
	def get_time(self, soup):
		thead = soup.find("td", "thead")
		if thead:
			return(" ".join([time.string for time in thead.findAll("strong")]))
		else:
			return("indefinite")
	
	def vote(self, options):
		"""Votes for a given option. Multiple options can be given in case of
		a poll allowing more than one choice."""
		
		if not self.vote_allowed:
			raise(PollError("Thread has no poll, not allowed to vote in poll or already voted in this poll."))
		
		if isinstance(options, list):
			if len(options) > 1 and not self.multiple:
				# Form would handle this but it's better to just raise
				raise(PollError("Cannot select multiple options."))
		else:
			options = [options]
		
		for option in self.options:
			if option in options:
				self.form.get("cb_optionnumber_{0}".format(option.number)).set()
		
		self.form.submit()
	
	def __eq__(self, other):
		if isinstance(other, Poll):
			# thread -> id conversion is more expensive than id -> thread (page)
			if self._id and other._id:
				return(self.id == other.id)
			else:
				return(self.thread.id == other.thread.id)
		elif isinstance(other, int):
			return(self.id == other)
		elif isinstance(other, str):
			return(self.question == other)
		else:
			return(False)
	
	def __str__(self):
		return(self.question)
	
	def __int__(self):
		return(self.id)
	
	def __repr__(self):
		return("<Poll {self.id} \"{self.question}\" in t{self.thread.id}>".format(self=self))

class PollOption(forumapi.ExtensibleObject):
	"""It's a poll option. It knows the poll it is in and the name, and may
	know its number, the number of votes it has and the percentage."""
	
	@property
	def obj(cls, poll, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], int):
			return(cls(poll.session, poll, *a[1:], number=a[0], **kw))
		else:
			return(None)
	
	def __init__(self, session, poll, number=None, name=None, votes=None, percentage=None, voters=None):
		if not number:
			raise(ValueError("Number required."))
		
		self.session = session
		self.poll = Poll.obj(session, poll)
		
		self._number = number
		self._name = name
		
		self._votes = votes
		self._percentage = percentage
		
		if isinstance(voters, list):
			self._voters = [self.module.get_attr("User").obj(self.session, user) for user in voters]
		else:
			self._voters = None
		
		self._find_instances()
	
	def _merge(self, other):
		self._merge_obj(other, "_name")
		
		if self._votes is not None:
			other._votes = self._votes
		if self._percentage is not None:
			other._percentage = self._percentage
		
		self._merge_obj(other, "_voters")
	
	@property
	def data(self):
		return({
			"poll": int(self.poll),
			"number": self._number,
			"name": forumapi._flatten_data(self._name),
			"votes": self._votes,
			"percentage": self._percentage,
			"voters": [int(user) for user in self.voters] if self._voters is not None else None,
		})
	
	@property
	def name(self):
		if self._name is not None:
			return(self._name)
		else:
			self.poll.retrieve_results()
			return(self._name)
	
	@property
	def votes(self):
		if self._votes is not None:
			return(self._votes)
		else:
			self.poll.retrieve_results()
			return(self._votes)
	
	@property
	def percentage(self):
		if self._percentage is not None:
			return(self._percentage)
		else:
			self.poll.retrieve_results()
			return(self._percentage)
	
	@property
	def voters(self):
		if self._voters is not None:
			return(self._voters)
		else:
			self.poll.retrieve_results()
			return(self._voters)
	
	def __eq__(self, other):
		if isinstance(other, PollOption):
			# Make sure poll parents are equal
			# Also, we can't equal on name because those might not be unique
			return(self.poll == other.poll and self.number == other.number)
		elif isinstance(other, int):
			return(self.number == other)
		elif isinstance(other, str):
			return(self.name == other)
		else:
			return(False)
	
	def __str__(self):
		return(self.name)
	
	def __int__(self):
		return(self.number)
	
	def __repr__(self):
		return("<PollOption {self.number} \"{self.name}\" in {0}, {1}>".format("p{self.poll.id}".format(self=self), "t{self.poll.thread.id}".format(self=self), self=self))
